﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using RapidRepository;

namespace RapidRepository.Cache
{
    /// <summary>
    /// Stores a cache of entities.
    /// </summary>
    public static class EntityCache
    {
        #region Declarations

        static object syncRoot = new object();
        internal static List<TypeCache> cachedEntities = null;
        internal static List<Type> eagerLoadedTypes = null;        
        static Dictionary<Type, Guid> entitiesToRemove;
        internal static List<Type> noCache = null;
        static internal int sleep = 0;

        #endregion

        #region Add

        /// <summary>
        /// Adds the specified id, if the entity already exists it will over write it in the cache.
        /// </summary>
        /// <param name="entityType">Type of the entity.</param>
        /// <param name="entityId">The entity id.</param>
        /// <param name="entity">The entity.</param>
        internal static void Add(Type entityType, Guid entityId, object entity)
        {
            Initialise();
            InitialiseEagerLoadedTypesList();
            InitialiseNoCacheList();

            if (!CacheAllowed(entityType))
            {
                return;
            }

            if (cachedEntities != null)
            {
                lock (syncRoot)
                {
                    if (cachedEntities != null)
                    {
                        AddEntityToCache(entityType, entityId, entity);
                    }
                }
            }
        } 

        #endregion

        #region Update

        /// <summary>
        /// Updates the specified entity type.
        /// </summary>
        /// <param name="entityType">Type of the entity.</param>
        /// <param name="entityId">The entity id.</param>
        /// <param name="entity">The entity.</param>
        internal static void Update(Type entityType, Guid entityId, object entity)
        {
            Initialise();
            InitialiseEagerLoadedTypesList();
            InitialiseNoCacheList();

            if (!CacheAllowed(entityType))
            {
                return;
            }

            if (cachedEntities != null)
            {
                lock (syncRoot)
                {
                    if (cachedEntities != null)
                    {
                        UpdateCachedEntity(entityType, entityId, entity);
                    }
                }
            }
        } 

        #endregion

        #region Remove

        /// <summary>
        /// Removes the specified entity id from the cache.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        /// <param name="entityId">The entity id.</param>
        internal static void Remove(Type entityType, Guid entityId)
        {
            Initialise();
            InitialiseEagerLoadedTypesList();
            InitialiseNoCacheList();
            InitialiseEntitiesToRemoveList();

            if (!CacheAllowed(entityType))
            {
                return;
            }

            if (cachedEntities != null)
            {
                lock (syncRoot)
                {
                    if (cachedEntities != null)
                    {
                        if(!cachedEntities.Any(x => x.EntityType == entityType))
                        {
                            return;
                        }

                        TypeCache typeCache = cachedEntities.Where(x => x.EntityType == entityType).First();

                        if (!typeCache.Entities.Any(x => x.EntityId == entityId))
                        {
                            return;
                        }

                        typeCache.Entities.Remove(typeCache.Entities.Where(x => x.EntityId == entityId).First());

                        SaveForRemovalIfEagerLoadingIncomplete(entityType, entityId, typeCache);
                    }
                }
            }
        }

        #endregion

        #region GetById

        /// <summary>
        /// Gets the by id.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        /// <param name="entityId">The entity id.</param>
        internal static TEntity GetById<TEntity>(Guid entityId)
        {
            if (cachedEntities != null)
            {
                lock (syncRoot)
                {
                    if (cachedEntities != null)
                    {
                        if(!cachedEntities.Any(x => x.EntityType == typeof(TEntity)))
                        {
                            return default(TEntity);
                        }

                        TypeCache typeCache = cachedEntities.Where(x => x.EntityType == typeof(TEntity)).First();

                        if (!typeCache.Entities.Any(x => x.EntityId == entityId))
                        {
                            return default(TEntity);
                        }

                        return (TEntity)typeCache.Entities.Where(x => x.EntityId == entityId).First().Entity;  
                    }
                }
            }
            return default(TEntity);
        } 

        #endregion

        #region GetAll

        /// <summary>
        /// Gets all entities of the specified type.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        internal static IList<TEntity> GetAll<TEntity>()
        {
            IList<TEntity> entities = new List<TEntity>();

            if (cachedEntities != null)
            {
                lock (syncRoot)
                {
                    if (cachedEntities != null)
                    {
                        if (!cachedEntities.Any(x => x.EntityType == typeof(TEntity)))
                        {
                            return entities;
                        }

                        var typeCache = cachedEntities.Where(x => x.EntityType == typeof(TEntity)).First();

                        if (typeCache.IsDirty)
                        {
                            return entities;
                        }

                        List<CachedEntity> cachedEntityList = typeCache.Entities;

                        foreach (var cachedEntity in cachedEntityList)
                        {
                            entities.Add((TEntity)cachedEntity.Entity);
                        }
                    }
                }
            }
            return entities;
        }

        #endregion

        #region EagerLoad

        /// <summary>
        /// Eager loads all the entities of the specified type into the cache.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        public static void EagerLoad<TEntity>()
        {
            Initialise();
            InitialiseEagerLoadedTypesList();
            InitialiseNoCacheList();
            InitialiseEntitiesToRemoveList();

            if (eagerLoadedTypes.Contains(typeof(TEntity)))
            {
                return;
            }

            TypeCache typeCache = null;

            lock (syncRoot)
            {
                if(noCache.Contains(typeof(TEntity)))
                {
                    throw new InvalidOperationException("You cannot eager load entities into the cache if their type has been added to the noCache collection.");
                }

                typeCache = cachedEntities.Where(x => x.EntityType == typeof(TEntity)).FirstOrDefault();

                if (typeCache != null)
                {
                    typeCache.IsDirty = true;
                }
                else
                {
                    CreateTypeCache(typeof(TEntity), new List<CachedEntity>());
                    typeCache = cachedEntities.Where(x => x.EntityType == typeof(TEntity)).First();
                    typeCache.IsDirty = true;
                }
            }

            ThreadStart threadStart = new ThreadStart(EagerLoadAsync<TEntity>);
            Thread thread = new Thread(threadStart);
            thread.Start();
            lock (syncRoot)
            {
                eagerLoadedTypes.Add(typeof(TEntity));
            }
        }

        #endregion

        #region NoCache

        /// <summary>
        /// Adds a type to the no cache list - this means that when entities are saved, updated or loaded, they are not added to the cache.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        public static void NoCache<TEntity>()
        {
            Initialise();
            InitialiseEagerLoadedTypesList();
            InitialiseNoCacheList();

            if (noCache.Contains(typeof(TEntity)))
            {
                return;
            }

            lock (syncRoot)
            {
                if(eagerLoadedTypes.Contains(typeof(TEntity)))
                {
                    throw new InvalidOperationException("You cannot specify a type for no caching if you are eager loading that type into the cache");
                }

                noCache.Add(typeof(TEntity));
            }
        }

        #endregion

        #region CacheAllowed

        /// <summary>
        /// Returns a bool indicating whether caching is allowed for the specified type.
        /// </summary>
        /// <param name="entityType">Type of the entity.</param>
        internal static bool CacheAllowed(Type entityType)
        {
            Initialise();
            InitialiseNoCacheList();
            lock (syncRoot)
            {
                return !noCache.Contains(entityType);
            }
        } 

        #endregion

        #region Private Methods

        private static void Initialise()
        {
            if (cachedEntities == null)
            {
                lock (syncRoot)
                {
                    if (cachedEntities == null)
                    {
                        cachedEntities = new List<TypeCache>();
                    }
                }
            }
        }

        private static void InitialiseEagerLoadedTypesList()
        {
            if (eagerLoadedTypes == null)
            {
                lock (syncRoot)
                {
                    if (eagerLoadedTypes == null)
                    {
                        eagerLoadedTypes = new List<Type>();
                    }
                }
            }
        }

        private static void InitialiseNoCacheList()
        {
            if (noCache == null)
            {
                lock (syncRoot)
                {
                    if (noCache == null)
                    {
                        noCache = new List<Type>();
                    }
                }
            }
        }

        private static void InitialiseEntitiesToRemoveList()
        {
            if (entitiesToRemove == null)
            {
                lock (syncRoot)
                {
                    if (entitiesToRemove == null)
                    {
                        entitiesToRemove = new Dictionary<Type, Guid>();
                    }
                }
            }
        }

        private static void SaveForRemovalIfEagerLoadingIncomplete(Type entityType, Guid entityId, TypeCache typeCache)
        {
            if (typeCache.IsDirty)
            {
                entitiesToRemove.Add(entityType, entityId);
            }
        }

        private static void EagerLoadAsync<TEntity>()
        {
            IFileManager fileManager = new FileManager();
            ISerialiser serialiser = new Serialiser();

            string[] entityList = fileManager.LoadFiles<TEntity>();

            foreach (var entity in entityList)
            {
                if (sleep > 0)
                {
                    Thread.Sleep(sleep);
                }

                var actualEntity = (IRapidEntity)serialiser.Deserialise<TEntity>(entity);
                EntityCache.Add(typeof(TEntity), actualEntity.Id, actualEntity);
            }

            lock (syncRoot)
            {
                var typeCache = cachedEntities.Where(x => x.EntityType == typeof(TEntity)).First();
                typeCache.IsDirty = false;

                RemoveItemsReAddedByEagerLoadingButShouldBeRemoved();
            }
            RapidContext.CurrentContext.SetEntityLoaded<TEntity>();
        }

        private static void RemoveItemsReAddedByEagerLoadingButShouldBeRemoved()
        {
            foreach (var item in entitiesToRemove)
            {
                Remove(item.Key, item.Value);
            }
            entitiesToRemove.Clear();
        }

        private static void AddEntityToCache(Type entityType, Guid entityId, object entity)
        {
            List<CachedEntity> entities = null;

            if (cachedEntities.Any(x => x.EntityType == entityType))
            {
                entities = cachedEntities.Where(x => x.EntityType == entityType).First().Entities;
            }
            else
            {
                entities = CreateTypeCache(entityType, entities);
            }

            if (!entities.Any(x => x.EntityId == entityId))
            {
                entities.Add(new CachedEntity { EntityId = entityId, Entity = entity });
            }
            else
            {
                var typeCache = cachedEntities.Where(x => x.EntityType == entityType).First();
                if (!typeCache.IsDirty)
                {
                    var existingEntity = entities.Where(x => x.EntityId == entityId).First();
                    existingEntity.Entity = entity;
                }
            }
        }

        private static void UpdateCachedEntity(Type entityType, Guid entityId, object entity)
        {
            if (cachedEntities.Any(x => x.EntityType != entityType))
            {
                return;
            }

            var typeCache = cachedEntities.Where(x => x.EntityType == entityType).First();

            var entities = typeCache.Entities;
            
            if (entities.Any(x => x.EntityId == entityId))
            {
                var existingEntity = entities.Where(x => x.EntityId == entityId).First();
                existingEntity.Entity = entity;
            }
        }

        private static List<CachedEntity> CreateTypeCache(Type entityType, List<CachedEntity> entities)
        {
            entities = new List<CachedEntity>();
            cachedEntities.Add(new TypeCache { EntityType = entityType, Entities = entities });
            return entities;
        }

        #endregion
    }
}
